package com.glodon.paas.databag.service.impl;

import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;

import javax.annotation.PostConstruct;
import javax.imageio.ImageIO;

import jsonij.json.JPath;
import jsonij.json.JSON.Array;
import jsonij.json.Value;
import jsonij.parser.ParserException;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.dozer.Mapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.glodon.paas.databag.api.bean.DatabagDto;
import com.glodon.paas.databag.api.bean.ResourceDto;
import com.glodon.paas.databag.biz.DispatcherBiz;
import com.glodon.paas.databag.biz.RevitConstructionBiz;
import com.glodon.paas.databag.biz.ThumbnailBiz;
import com.glodon.paas.databag.dao.DatabagRepository;
import com.glodon.paas.databag.dao.DatabagTaskRepository;
import com.glodon.paas.databag.dao.ThumbnailRepository;
import com.glodon.paas.databag.model.Databag;
import com.glodon.paas.databag.model.DatabagTask;
import com.glodon.paas.databag.model.Thumbnail;
import com.glodon.paas.databag.service.DatabagService;
import com.glodon.paas.databag.util.WebService;
import com.glodon.paas.exception.PaasServerExceptionFactory;
import com.glodon.paas.job.model.TaskStatus;
import com.glodon.paas.util.TokenGenerator;
import com.glodon.paas.util.UUIDUtil;

@Service
public class DatabagServiceImpl implements DatabagService {
    private final Logger logger = LoggerFactory.getLogger(DatabagServiceImpl.class);
    private final DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
    private final String STORE_PREFIX = "Databag-";
    private final String DEFAULT_MIME = "application/octet-stream";
    private final String PATH_SEPARATOR = "/";
    private Properties mimeProperties = new Properties();
    private final Map<String, String> types = new HashMap<String, String>();

    @org.springframework.beans.factory.annotation.Value("${manifestFile:manifest.json}")
    private String manifestFile;
    @org.springframework.beans.factory.annotation.Value("${document.storage.url}")
    private String documentUrl;
    @org.springframework.beans.factory.annotation.Value("${databag.type}")
    private String databagType;

    @Autowired
    private DatabagRepository databagRepo;
    @Autowired
    private DatabagTaskRepository taskRepo;
    @Autowired
    private WebService storage;
    @Autowired
    private DispatcherBiz dispatcherBiz;
    @Autowired
    private ThumbnailBiz thumbnailBiz;
    @Autowired
    private ThumbnailRepository thumbnailRepo;
    @Autowired
    private RevitConstructionBiz revitBiz;
    @Autowired
    private Mapper mapper;

    @Override
    @Transactional
    public DatabagDto create(DatabagDto dto) {
        String type = types.get(dto.getType());
        if (type == null) {
            throw PaasServerExceptionFactory.unImplementedException("not support databag type:" + dto.getType(), null);
        }
        dto.setType(type);
        Databag databag = mapper.map(dto, Databag.class);
        databag.setOutput(getOutputPath());
        String token = databag.getToken();
        if (StringUtils.isNotEmpty(token)) {// token不为空，则断开token与file以前版本的关联
            databagRepo.clearPublicByToken(token);
        }
        databagRepo.save(databag);// token为空，则无需公开，token不为空，则该token与新数据包关联
        if (type.equals("image")) {// 生成thumbnail的本地worker
            generateImageThumbnail(databag);
            databag.setEndTime(new Date());
            databag.setSuccess(true);
            databagRepo.save(databag);
        } else {
            DatabagTask task = buildTask(databag);
            taskRepo.save(task);
            dispatcherBiz.dispatch(task);
            taskRepo.save(task);
        }
        return mapper.map(databag, DatabagDto.class);
    }

    @Override
    public DatabagDto get(String databagId) {
        Databag bag = databagRepo.findOne(databagId);
        if (bag == null) {
            throw PaasServerExceptionFactory.notFoundException("invalid databag id:" + databagId, null);
        }
        return mapper.map(bag, DatabagDto.class);
    }

    @Override
    public List<DatabagDto> findByMetadata(String source, String localId, String version) {
        List<Databag> bags = databagRepo.findBySourceAndLocalIdAndVersion(source, localId, version);
        List<DatabagDto> bagList = new ArrayList<DatabagDto>();
        for (Databag databagJob : bags) {
            bagList.add(mapper.map(databagJob, DatabagDto.class));
        }
        return bagList;
    }

    @Override
    public ResourceDto getThumbnail(String databagId, String size) {
        // 第一步，找匹配的thumbnail
        List<Thumbnail> thumbnails = thumbnailRepo.findByDatabagIdAndSize(databagId, size);
        if (!thumbnails.isEmpty()) {
            return getResourceByPath(databagId, thumbnails.get(0).getPath());
        }
        // 第一步没有找到，进行第二步，找最接近的thumbnail
        thumbnails = thumbnailRepo.findByDatabagIdOrderBySizeAsc(databagId);
        Thumbnail matched = findMatchestSizeThumbnail(thumbnails, size);
        if (matched != null) {
            ResourceDto dto = getResourceByPath(databagId, matched.getPath());
            InputStream scaledIs = scaleThumbnailSize(dto.getInputStream(), matched.getWidth(), matched.getHeight(),
                    size);
            dto.setInputStream(scaledIs);
            return dto;
        }
        // 由于是第一次获取thumbnail，所以第二步也没有找到，从manifest.json中生成thumbnail
        thumbnails = parseThumbnail(databagId);
        thumbnailRepo.save(thumbnails);
        matched = findMatchestSizeThumbnail(thumbnails, size);
        ResourceDto dto = getResourceByPath(databagId, matched.getPath());
        InputStream scaledIs = scaleThumbnailSize(dto.getInputStream(), matched.getWidth(), matched.getHeight(), size);
        dto.setInputStream(scaledIs);
        return dto;
    }

    @Override
    public ResourceDto getResource(String databagId) {
        Databag databag = databagRepo.findOne(databagId);
        if (databag == null) {
            throw PaasServerExceptionFactory.notFoundException("invalid databagId:" + databagId, null);
        }
        ResourceDto resource = storage.downloadResource(appendUrlParam(databag.getInput(),
                "userId=" + databag.getUserId()));
        String contentType = getContentType(resource.getName());
        resource.setMime(contentType);
        return resource;
    }

    /**
     * 解析thumbnail信息.
     * 
     * @param databagId
     * @return
     */
    private List<Thumbnail> parseThumbnail(String databagId) {
        String manifest = getManifest(databagId);
        try {
            String defaultViewId = JPath.evaluate(manifest, "/Metadata/DefaultView").getString();
            if (defaultViewId == null) {
                logger.error("can't find DefaultViewId in {}: {}", manifestFile, manifest);
                throw PaasServerExceptionFactory.unknownException("can't find DefaultViewId.", null);
            }
            Value representations = JPath.evaluate(manifest, "/Views[?(@.ID=\"" + defaultViewId
                    + "\")]/Representations");
            if (representations == null) {
                logger.error("can't find Representations in DefaultView, view id: {}", defaultViewId);
                throw PaasServerExceptionFactory.unknownException("can't find Representations in DefaultView.", null);
            }
            @SuppressWarnings("rawtypes")
            Array repreArray = (Array) representations;
            List<Thumbnail> newThumbnails = new ArrayList<Thumbnail>();
            for (int i = 0; i < repreArray.size(); i++) {
                Value repre = repreArray.get(i);
                Value category = JPath.evaluate(repre, "/Attributes/Category");
                if (category != null && "Thumbnail".equals(category.getString())) {
                    int currentWidth = JPath.evaluate(repre, "/Attributes/Width").getInt();
                    int currentHeigth = JPath.evaluate(repre, "/Attributes/Height").getInt();
                    int currentSize = Math.max(currentWidth, currentHeigth);
                    String currentPath = JPath.evaluate(repre, "/Path").getString();
                    Thumbnail thumbnail = new Thumbnail(databagId, String.valueOf(currentSize), currentPath);
                    thumbnail.setWidth(currentWidth);
                    thumbnail.setHeight(currentHeigth);
                    newThumbnails.add(thumbnail);
                }
            }
            return newThumbnails;
        } catch (Exception e) {
            throw PaasServerExceptionFactory.notFoundException("parse thumbnail failed.", e);
        }
    }

    /**
     * 找到与目标size最匹配的thumbnail, 根据最小面积差来匹配.
     * 
     * @param thumbnails
     * @param size
     * @return
     */
    private Thumbnail findMatchestSizeThumbnail(List<Thumbnail> thumbnails, String size) {
        if (thumbnails.isEmpty()) {
            return null;
        }
        Thumbnail matched = null;
        int minDelta = Integer.MAX_VALUE;
        int targetArea = Integer.parseInt(size) * Integer.parseInt(size);
        for (Thumbnail thumbnail : thumbnails) {
            int realArea = thumbnail.getWidth() * thumbnail.getHeight();
            if ((realArea >= targetArea) && (realArea - targetArea < minDelta)) {
                minDelta = realArea - targetArea;
                matched = thumbnail;
            }
        }
        if (matched == null) {
            minDelta = Integer.MAX_VALUE;
            for (Thumbnail thumbnail : thumbnails) {
                int realArea = thumbnail.getWidth() * thumbnail.getHeight();
                if (Math.abs(realArea - targetArea) < minDelta) {
                    minDelta = Math.abs(realArea - targetArea);
                    matched = thumbnail;
                }
            }
        }
        return matched;
    }

    private InputStream scaleThumbnailSize(InputStream inputStream, int width, int height, String size) {
        if (width == height && width == Integer.parseInt(size)) {
            return inputStream;
        }
        try {
            BufferedImage image = ImageIO.read(inputStream);
            double dSize = Double.parseDouble(size);
            double rate = Math.min(dSize / width, dSize / height);
            Image target = image.getScaledInstance((int) (width * rate), (int) (height * rate), Image.SCALE_SMOOTH);
            BufferedImage bufferedImage = new BufferedImage((int) (width * rate), (int) (height * rate),
                    BufferedImage.TYPE_INT_BGR);
            Graphics2D g2d = bufferedImage.createGraphics();
            g2d.drawImage(target, 0, 0, null);
            g2d.dispose();
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            ImageIO.write(bufferedImage, "png", os);
            InputStream is = new ByteArrayInputStream(os.toByteArray());
            os.close();
            return is;
        } catch (IOException e) {
            throw PaasServerExceptionFactory.systemException("scale thumbnail failed.", e);
        }
    }

    @Override
    public List<String> getResourceByMime(String databagId, String mime) {
        String manifest = getManifest(databagId);
        try {
            String defaultViewId = JPath.evaluate(manifest, "/Metadata/DefaultView").getString();
            if (defaultViewId == null) {
                logger.error("can't find DefaultViewId in {}: {}", manifestFile, manifest);
                throw PaasServerExceptionFactory.unknownException("can't find DefaultViewId.", null);
            }
            Value view = JPath.evaluate(manifest, "/Views[?(@.ID=\"" + defaultViewId + "\")]");
            if (view == null) {
                logger.error("can't find default view by view id: {}", defaultViewId);
                throw PaasServerExceptionFactory.unknownException("can't find default view.", null);
            }
            Value repre = JPath.evaluate(view, "/Representations/[?(@.MIME=\"" + mime + "\")]");
            if (repre == null) {
                logger.error("can't find representation by mime: {}", mime);
                throw PaasServerExceptionFactory.unknownException("can't find representation.", null);
            }
            Value pathValue = JPath.evaluate(repre, "/Path");
            if (pathValue == null) {
                logger.error("can't find Path attribute: {}", repre.getString());
                throw PaasServerExceptionFactory.unknownException("can't find Path attribute.", null);
            }
            String mimePath = pathValue.getString();
            logger.debug("get mime resource: {}", mimePath);
            List<String> result = new ArrayList<String>();
            result.add(mimePath);
            return result;
        } catch (ParserException e) {
            throw PaasServerExceptionFactory.systemException("parse manifest.json failed.", e);
        } catch (IOException e) {
            throw PaasServerExceptionFactory.systemException("parse manifest.json failed.", e);
        }
    }

    @Override
    public ResourceDto getResourceByPath(String databagId, String fileName) {
        Databag bag = databagRepo.findOne(databagId);
        if (bag == null) {
            throw PaasServerExceptionFactory.notFoundException("invalid databag id:" + databagId, null);
        }
        if (bag.getEndTime() == null) {// 数据包还没有创建完成
            throw PaasServerExceptionFactory.notFoundException("databag[" + databagId
                    + "] havn't completed yet, wait and try again.", null);
        }
        String path = bag.getOutput();
        String key = path + PATH_SEPARATOR + fileName;
        InputStream is = storage.getResource(key);
        if (isLoadRevitManifest(bag.getType(), fileName)) {
            try {
                String manifest = IOUtils.toString(is);
                is.close();
                revitBiz.parseRevitConstruction(databagId, path, manifest);
                is = IOUtils.toInputStream(manifest);
            } catch (IOException e) {
                logger.error("failed to invoking async parse revit constructions.", e);
            } finally {

            }
        }
        ResourceDto resource = new ResourceDto();
        resource.setInputStream(is);
        String suffix = fileName.substring(fileName.lastIndexOf("/") + 1);
        resource.setName(suffix);
        String mime = getContentType(suffix);
        resource.setMime(mime);
        return resource;
    }

    @Override
    @Transactional
    public boolean deleteResource(final String databagId, final String alternatePublic) {
        try {
            String pkgPath = getOutputById(databagId);
            storage.deleteResource(pkgPath);
            thumbnailRepo.deleteThumbnails(databagId);
            taskRepo.deleteDatabagTasks(databagId);
            Databag databag = databagRepo.findOne(databagId);
            if (databag != null) {
                databagRepo.delete(databag);
                transferPublicToken(alternatePublic, databag.getToken());
            }
            logger.info("deleted databag: {}", databagId);
        } catch (Exception e) {
            logger.error("failed to delete databag in OSS:" + databagId, e);
            return false;
        }
        return true;
    }

    @Override
    public String getRevitConstruction(String databagId, String constructionId) {
        String construction = revitBiz.getRevitConstruction(databagId, constructionId);
        return construction;
    }

    @PostConstruct
    public void initMimeProperties() {
        try {
            mimeProperties = PropertiesLoaderUtils
                    .loadAllProperties("com/glodon/paas/databag/service/impl/mime.properties");
        } catch (IOException e) {
            logger.error("loading mime.properties failed.", e);
        }
    }

    /**
     * 如果删除的数据包上有公开的token，需要将token转移到同一文件上留存的最新版本上
     * 
     * @param databag
     */
    private void transferPublicToken(String alternatePublic, String token) {
        if (StringUtils.isEmpty(alternatePublic) || StringUtils.isEmpty(token)) {
            return;
        }
        databagRepo.setPublicById(alternatePublic, token);
    }

    private void generateImageThumbnail(Databag databag) {
        String file = storage.downloadFile(appendUrlParam(databag.getInput(), "userId=" + databag.getUserId()));
        String tempDir = new File(file).getParent() + File.separator + databag.getId();
        List<String> thumbnails = thumbnailBiz.createImageDatabag(file, tempDir);
        List<Thumbnail> thumbs = new ArrayList<Thumbnail>();
        for (String thumbnail : thumbnails) {
            InputStream is = null;
            try {
                File thumbnailFile = new File(tempDir + File.separator + thumbnail);
                is = new FileInputStream(thumbnailFile);
                long length = thumbnailFile.length();
                storage.postResource(databag.getOutput() + "/" + thumbnail, is, length);
            } catch (IOException e) {
                logger.error("failed to upload thumbnail:" + thumbnail, e);
                continue;
            } finally {
                if (is != null) {
                    try {
                        is.close();
                    } catch (IOException e) {
                    }
                }
            }
            String wh = thumbnail.substring(thumbnail.lastIndexOf("-") + 1);
            wh = StringUtils.substringBefore(wh, ".");
            String[] split = StringUtils.split(wh, "x");
            int width = Integer.parseInt(split[0]);
            int height = Integer.parseInt(split[1]);
            String size = String.valueOf(Math.max(width, height));
            Thumbnail thumb = new Thumbnail(databag.getId(), size, thumbnail);
            thumb.setWidth(width);
            thumb.setHeight(height);
            thumbs.add(thumb);
        }
        thumbnailRepo.save(thumbs);
    }

    /**
     * 根据databagId查找在OSS中的输出路径.
     * 
     * @param databagId
     * @return
     */
    private String getOutputById(String databagId) {
        Databag job = databagRepo.findOne(databagId);
        if (job == null) {
            throw PaasServerExceptionFactory.notFoundException("invalid databag id:" + databagId, null);
        }
        String outputPath = job.getOutput();
        if (outputPath == null) {
            throw PaasServerExceptionFactory.notFoundException("Creation of databag isn't complete yet.", null);
        }
        return outputPath;
    }

    /**
     * 是否加载的资源为Revit的manifest.json文件. 如果是，则后台异步加载revit的构建属性.
     * 
     * @param databagType
     * @param fileName
     */
    private boolean isLoadRevitManifest(String databagType, String fileName) {
        if (!"manifest.json".equalsIgnoreCase(fileName)) {
            return false;
        }
        if ("rvt".equalsIgnoreCase(databagType)) {
            return true;
        }
        return false;
    }

    private String getManifest(String databagId) {
        ResourceDto dto = getResourceByPath(databagId, manifestFile);
        InputStream is = dto.getInputStream();
        String manifest = null;
        try {
            manifest = IOUtils.toString(is);
            return manifest;
        } catch (IOException e1) {
            throw PaasServerExceptionFactory.systemException("error when getting " + manifestFile, e1);
        }
    }

    /**
     * 产生output的路径.
     * 
     * @return
     */
    private String getOutputPath() {
        return PATH_SEPARATOR + STORE_PREFIX + format.format(new Date()) + PATH_SEPARATOR + UUIDUtil.getUUIDStr();
    }

    /**
     * 根据文件后缀名获取MIME类型.
     * 
     * @param fileName
     *            文件名
     * @return
     */
    private String getContentType(String fileName) {
        if (fileName == null || fileName.lastIndexOf(".") < 0) {
            return DEFAULT_MIME;
        }
        String suffix = fileName.substring(fileName.lastIndexOf(".") + 1);
        String contentType = mimeProperties.getProperty(suffix, DEFAULT_MIME);
        return contentType;
    }

    private DatabagTask buildTask(Databag bag) {
        DatabagTask task = new DatabagTask();
        task.setDatabagId(bag.getId());
        task.setInput(appendUrlParam(bag.getInput(), "userId=" + bag.getUserId()));
        // task.setUserId(job.getUserId());
        task.setStatus(TaskStatus.created);
        task.setOutput(documentUrl.trim() + bag.getOutput());
        task.setType(bag.getType());
        return task;
    }

    public void setDispatcherBiz(DispatcherBiz dispatcherBiz) {
        this.dispatcherBiz = dispatcherBiz;
    }

    public void setMapper(Mapper mapper) {
        this.mapper = mapper;
    }

    public void setTaskRepo(DatabagTaskRepository taskRepo) {
        this.taskRepo = taskRepo;
    }

    public void setDatabagRepo(DatabagRepository pkgJobRepository) {
        this.databagRepo = pkgJobRepository;
    }

    public void setStorage(WebService storage) {
        this.storage = storage;
    }

    public void setManifestFile(String manifestFile) {
        this.manifestFile = manifestFile;
    }

    public void setThumbnailRepo(ThumbnailRepository thumbnailRepo) {
        this.thumbnailRepo = thumbnailRepo;
    }

    /**
     * 在url后面追加一个参数.
     * 
     * @param url
     * @param keyValueParam
     * @return
     */
    private String appendUrlParam(String url, String keyValueParam) {
        if (url.indexOf("?") < 0) {
            return url + "?" + keyValueParam;
        } else if (url.endsWith("?")) {
            return url + keyValueParam;
        } else {
            return url + "&" + keyValueParam;
        }
    }

    @PostConstruct
    public void parseDocType() {
        if (StringUtils.isEmpty(databagType)) {
            throw PaasServerExceptionFactory.systemException("not found or bad 'databag.type' in default.properties",
                    null);
        }
        String[] rawTypes = StringUtils.split(databagType, ";");
        for (int i = 0; i < rawTypes.length; i++) {
            String[] rawType = StringUtils.split(rawTypes[i], ":");
            String type = rawType[0].trim();
            String[] suffixes = StringUtils.split(rawType[1], ",");
            for (int j = 0; j < suffixes.length; j++) {
                types.put(suffixes[j].toLowerCase(), type);
            }
        }
    }

    @Override
    @Transactional
    public String publicDatabag(String databagId) {
        Databag bag = databagRepo.findOne(databagId);
        if (bag == null) {
            throw PaasServerExceptionFactory.notFoundException("invalid databag id:" + databagId, null);
        }
        if (StringUtils.isNotEmpty(bag.getToken())) {
            logger.info("databag '{}' has already public.", databagId);
            return bag.getToken();
        }
        String token = TokenGenerator.generate(UUID.randomUUID().toString());
        bag.setToken(token);
        databagRepo.save(bag);
        return token;
    }

    @Override
    @Transactional
    public void revokeDatabag(String databagId) {
        int success = databagRepo.clearPublicById(databagId);
        if (success > 0) {
            logger.info("success revoked databag '{}' public.", databagId);
        }
    }

    @Override
    public DatabagDto findByToken(String token) {
        Databag databag = databagRepo.findByToken(token);
        if (databag != null) {
            return mapper.map(databag, DatabagDto.class);
        }
        return null;
    }

    @Override
    public String getDatabagType(String suffix) {
        return types.get(suffix.trim().toLowerCase());
    }
}
